arkaos 3.14.0 → 3.15.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/core/__pycache__/favorites.cpython-313.pyc +0 -0
- package/core/favorites.py +91 -0
- package/dashboard/app/composables/useFavorites.ts +73 -0
- package/dashboard/app/pages/agents/[id].vue +20 -6
- package/dashboard/app/pages/agents/index.vue +29 -0
- package/dashboard/app/pages/personas/[id].vue +12 -0
- package/dashboard/app/pages/personas/index.vue +28 -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 +14 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.15.0
|
|
Binary file
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Favorites store for agents and personas (PR86a v3.15.0).
|
|
2
|
+
|
|
3
|
+
Single JSON file at ``~/.arkaos/favorites.json`` shaped as
|
|
4
|
+
``{"agents": ["<id>", ...], "personas": ["<id>", ...]}``.
|
|
5
|
+
|
|
6
|
+
Survives across sessions, mutated by the dashboard. No tier-0 protection
|
|
7
|
+
needed — favouriting is read-only intent.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
FavoriteKind = Literal["agents", "personas"]
|
|
17
|
+
_VALID_KINDS: tuple[str, ...] = ("agents", "personas")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _store_path() -> Path:
|
|
21
|
+
return Path.home() / ".arkaos" / "favorites.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load() -> dict[str, list[str]]:
|
|
25
|
+
path = _store_path()
|
|
26
|
+
if not path.exists():
|
|
27
|
+
return {"agents": [], "personas": []}
|
|
28
|
+
try:
|
|
29
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
30
|
+
except (OSError, json.JSONDecodeError):
|
|
31
|
+
return {"agents": [], "personas": []}
|
|
32
|
+
if not isinstance(data, dict):
|
|
33
|
+
return {"agents": [], "personas": []}
|
|
34
|
+
return {
|
|
35
|
+
"agents": [str(x) for x in (data.get("agents") or []) if isinstance(x, str)],
|
|
36
|
+
"personas": [str(x) for x in (data.get("personas") or []) if isinstance(x, str)],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _save(state: dict[str, list[str]]) -> None:
|
|
41
|
+
path = _store_path()
|
|
42
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
tmp = path.with_suffix(".json.tmp")
|
|
44
|
+
tmp.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
45
|
+
tmp.replace(path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_favorites() -> dict[str, list[str]]:
|
|
49
|
+
"""Return the current favourites payload."""
|
|
50
|
+
return _load()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_favorite(kind: str, item_id: str) -> bool:
|
|
54
|
+
if kind not in _VALID_KINDS:
|
|
55
|
+
return False
|
|
56
|
+
state = _load()
|
|
57
|
+
return item_id in state.get(kind, [])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def toggle(kind: str, item_id: str) -> dict:
|
|
61
|
+
"""Flip the favourite state. Returns ``{kind, id, favorited}``."""
|
|
62
|
+
if kind not in _VALID_KINDS:
|
|
63
|
+
return {"error": f"unknown kind: {kind!r}"}
|
|
64
|
+
if not item_id:
|
|
65
|
+
return {"error": "id is required"}
|
|
66
|
+
state = _load()
|
|
67
|
+
bucket = state.setdefault(kind, [])
|
|
68
|
+
if item_id in bucket:
|
|
69
|
+
bucket.remove(item_id)
|
|
70
|
+
favorited = False
|
|
71
|
+
else:
|
|
72
|
+
bucket.append(item_id)
|
|
73
|
+
favorited = True
|
|
74
|
+
state[kind] = bucket
|
|
75
|
+
_save(state)
|
|
76
|
+
return {"kind": kind, "id": item_id, "favorited": favorited}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_favorite(kind: str, item_id: str, favorited: bool) -> dict:
|
|
80
|
+
"""Force a specific state. Useful for tests / bulk operations."""
|
|
81
|
+
if kind not in _VALID_KINDS:
|
|
82
|
+
return {"error": f"unknown kind: {kind!r}"}
|
|
83
|
+
state = _load()
|
|
84
|
+
bucket = state.setdefault(kind, [])
|
|
85
|
+
if favorited and item_id not in bucket:
|
|
86
|
+
bucket.append(item_id)
|
|
87
|
+
elif not favorited and item_id in bucket:
|
|
88
|
+
bucket.remove(item_id)
|
|
89
|
+
state[kind] = bucket
|
|
90
|
+
_save(state)
|
|
91
|
+
return {"kind": kind, "id": item_id, "favorited": favorited}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// PR86a v3.15.0 — shared favourites state + toggle helper.
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for the dashboard so the star button in
|
|
4
|
+
// agents/personas detail pages stays in sync with the filter chip
|
|
5
|
+
// on the index tables.
|
|
6
|
+
|
|
7
|
+
import { createSharedComposable } from '@vueuse/core'
|
|
8
|
+
|
|
9
|
+
interface FavoritesPayload {
|
|
10
|
+
agents: string[]
|
|
11
|
+
personas: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const _useFavorites = () => {
|
|
15
|
+
const { apiBase } = useApi()
|
|
16
|
+
const toast = useToast()
|
|
17
|
+
const state = useState<FavoritesPayload>('favorites', () => ({
|
|
18
|
+
agents: [],
|
|
19
|
+
personas: [],
|
|
20
|
+
}))
|
|
21
|
+
const loaded = useState<boolean>('favoritesLoaded', () => false)
|
|
22
|
+
|
|
23
|
+
async function load(force = false) {
|
|
24
|
+
if (loaded.value && !force) return
|
|
25
|
+
try {
|
|
26
|
+
const res = await $fetch<FavoritesPayload>(`${apiBase}/api/favorites`)
|
|
27
|
+
state.value = {
|
|
28
|
+
agents: res.agents ?? [],
|
|
29
|
+
personas: res.personas ?? [],
|
|
30
|
+
}
|
|
31
|
+
loaded.value = true
|
|
32
|
+
} catch {
|
|
33
|
+
// Best-effort — leave defaults
|
|
34
|
+
loaded.value = true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isAgentFavorite(id: string): boolean {
|
|
39
|
+
return state.value.agents.includes(id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isPersonaFavorite(id: string): boolean {
|
|
43
|
+
return state.value.personas.includes(id)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function toggle(kind: 'agents' | 'personas', id: string) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await $fetch<{ favorited?: boolean, error?: string }>(
|
|
49
|
+
`${apiBase}/api/favorites/${kind}/${id}`,
|
|
50
|
+
{ method: 'POST' },
|
|
51
|
+
)
|
|
52
|
+
if (res.error) throw new Error(res.error)
|
|
53
|
+
const bucket = state.value[kind]
|
|
54
|
+
if (res.favorited && !bucket.includes(id)) {
|
|
55
|
+
state.value = { ...state.value, [kind]: [...bucket, id] }
|
|
56
|
+
} else if (!res.favorited) {
|
|
57
|
+
state.value = { ...state.value, [kind]: bucket.filter((x) => x !== id) }
|
|
58
|
+
}
|
|
59
|
+
return res.favorited
|
|
60
|
+
} catch (err) {
|
|
61
|
+
toast.add({
|
|
62
|
+
title: 'Favorite toggle failed',
|
|
63
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
64
|
+
color: 'error',
|
|
65
|
+
})
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { state, load, isAgentFavorite, isPersonaFavorite, toggle, loaded }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const useFavorites = createSharedComposable(_useFavorites)
|
|
@@ -59,6 +59,10 @@ function formatRelative(iso: string | null): string {
|
|
|
59
59
|
return `${months}mo ago`
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// PR86a v3.15.0 — favorites.
|
|
63
|
+
const favs = useFavorites()
|
|
64
|
+
await favs.load()
|
|
65
|
+
|
|
62
66
|
// PR76 — edit drawer state
|
|
63
67
|
const editOpen = ref(false)
|
|
64
68
|
|
|
@@ -268,12 +272,22 @@ function formatTokens(n: number): string {
|
|
|
268
272
|
</h1>
|
|
269
273
|
<p class="text-base md:text-lg text-muted mt-0.5">{{ agent.role }}</p>
|
|
270
274
|
</div>
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
<div class="flex items-center gap-2">
|
|
276
|
+
<UButton
|
|
277
|
+
icon="i-lucide-star"
|
|
278
|
+
:color="favs.isAgentFavorite(agent.id) ? 'warning' : 'neutral'"
|
|
279
|
+
:variant="favs.isAgentFavorite(agent.id) ? 'soft' : 'ghost'"
|
|
280
|
+
size="sm"
|
|
281
|
+
:aria-label="favs.isAgentFavorite(agent.id) ? 'Unfavorite' : 'Favorite'"
|
|
282
|
+
@click="favs.toggle('agents', agent.id)"
|
|
283
|
+
/>
|
|
284
|
+
<UButton
|
|
285
|
+
label="Edit"
|
|
286
|
+
icon="i-lucide-pencil"
|
|
287
|
+
size="sm"
|
|
288
|
+
@click="openEditor"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
277
291
|
</div>
|
|
278
292
|
<div class="flex flex-wrap items-center gap-2 pt-1">
|
|
279
293
|
<UBadge :label="agent.department" variant="subtle" />
|
|
@@ -111,6 +111,10 @@ const filteredAgents = computed(() => {
|
|
|
111
111
|
result = result.filter(agent => String(agent.tier) === tierFilter.value)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
if (favoritesOnly.value) {
|
|
115
|
+
result = result.filter(agent => favs.isAgentFavorite(agent.id))
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
return result
|
|
115
119
|
})
|
|
116
120
|
|
|
@@ -139,6 +143,7 @@ const tierColor = (tier: number) => {
|
|
|
139
143
|
|
|
140
144
|
const columns: TableColumn<Agent>[] = [
|
|
141
145
|
{ id: 'select', header: '' },
|
|
146
|
+
{ id: 'favorite', header: '' },
|
|
142
147
|
{ accessorKey: 'name', header: 'Name' },
|
|
143
148
|
{ accessorKey: 'role', header: 'Role' },
|
|
144
149
|
{ accessorKey: 'department', header: 'Department' },
|
|
@@ -157,6 +162,11 @@ function goToAgent(id: string) {
|
|
|
157
162
|
navigateTo(`/agents/${id}`)
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
// PR86a v3.15.0 — favorites.
|
|
166
|
+
const favs = useFavorites()
|
|
167
|
+
await favs.load()
|
|
168
|
+
const favoritesOnly = ref(false)
|
|
169
|
+
|
|
160
170
|
// PR83b v3.4.0 — bulk selection + delete.
|
|
161
171
|
// PR84b v3.8.0 — bulk move department.
|
|
162
172
|
const confirmDialog = useConfirmDialog()
|
|
@@ -371,6 +381,15 @@ async function undoTrashIds(ids: string[]) {
|
|
|
371
381
|
aria-label="Filter by tier"
|
|
372
382
|
/>
|
|
373
383
|
|
|
384
|
+
<UButton
|
|
385
|
+
:label="favoritesOnly ? 'All' : 'Favorites'"
|
|
386
|
+
:icon="favoritesOnly ? 'i-lucide-star' : 'i-lucide-star'"
|
|
387
|
+
:color="favoritesOnly ? 'warning' : 'neutral'"
|
|
388
|
+
:variant="favoritesOnly ? 'soft' : 'outline'"
|
|
389
|
+
size="sm"
|
|
390
|
+
@click="favoritesOnly = !favoritesOnly"
|
|
391
|
+
/>
|
|
392
|
+
|
|
374
393
|
<span class="ml-auto text-xs text-muted">
|
|
375
394
|
{{ totalFiltered }} agent{{ totalFiltered !== 1 ? 's' : '' }}
|
|
376
395
|
</span>
|
|
@@ -404,6 +423,16 @@ async function undoTrashIds(ids: string[]) {
|
|
|
404
423
|
@click.stop
|
|
405
424
|
/>
|
|
406
425
|
</template>
|
|
426
|
+
<template #favorite-cell="{ row }">
|
|
427
|
+
<UButton
|
|
428
|
+
:icon="favs.isAgentFavorite(row.original.id) ? 'i-lucide-star' : 'i-lucide-star'"
|
|
429
|
+
:color="favs.isAgentFavorite(row.original.id) ? 'warning' : 'neutral'"
|
|
430
|
+
:variant="favs.isAgentFavorite(row.original.id) ? 'soft' : 'ghost'"
|
|
431
|
+
size="xs"
|
|
432
|
+
:aria-label="favs.isAgentFavorite(row.original.id) ? 'Unfavorite' : 'Favorite'"
|
|
433
|
+
@click.stop="favs.toggle('agents', row.original.id)"
|
|
434
|
+
/>
|
|
435
|
+
</template>
|
|
407
436
|
<template #name-cell="{ row }">
|
|
408
437
|
<button class="text-left font-medium text-primary hover:underline" @click="goToAgent(row.original.id)">
|
|
409
438
|
{{ row.original.name }}
|
|
@@ -226,6 +226,10 @@ function csvToList(value: string): string[] {
|
|
|
226
226
|
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
|
|
227
227
|
const suggestingField = ref<SuggestField | null>(null)
|
|
228
228
|
|
|
229
|
+
// PR86a v3.15.0 — favorites.
|
|
230
|
+
const favs = useFavorites()
|
|
231
|
+
await favs.load()
|
|
232
|
+
|
|
229
233
|
// PR85a v3.11.0 — Clone to Agent dialog.
|
|
230
234
|
const cloneOpen = ref(false)
|
|
231
235
|
function onCloned(agentId: string) {
|
|
@@ -473,6 +477,14 @@ const vocabOptions = [
|
|
|
473
477
|
</p>
|
|
474
478
|
</div>
|
|
475
479
|
<div class="flex items-center gap-2">
|
|
480
|
+
<UButton
|
|
481
|
+
icon="i-lucide-star"
|
|
482
|
+
:color="favs.isPersonaFavorite(detail.id) ? 'warning' : 'neutral'"
|
|
483
|
+
:variant="favs.isPersonaFavorite(detail.id) ? 'soft' : 'ghost'"
|
|
484
|
+
size="sm"
|
|
485
|
+
:aria-label="favs.isPersonaFavorite(detail.id) ? 'Unfavorite' : 'Favorite'"
|
|
486
|
+
@click="favs.toggle('personas', detail.id)"
|
|
487
|
+
/>
|
|
476
488
|
<UButton
|
|
477
489
|
label="Clone to Agent"
|
|
478
490
|
icon="i-lucide-copy-plus"
|
|
@@ -77,6 +77,9 @@ const filteredPersonas = computed<Persona[]>(() => {
|
|
|
77
77
|
return src === sourceFilter.value
|
|
78
78
|
})
|
|
79
79
|
}
|
|
80
|
+
if (favoritesOnly.value) {
|
|
81
|
+
result = result.filter((p) => favs.isPersonaFavorite(p.id))
|
|
82
|
+
}
|
|
80
83
|
return result
|
|
81
84
|
})
|
|
82
85
|
|
|
@@ -119,6 +122,7 @@ function agentCount(personaId: string): number {
|
|
|
119
122
|
|
|
120
123
|
const columns: TableColumn<Persona>[] = [
|
|
121
124
|
{ id: 'select', header: '' },
|
|
125
|
+
{ id: 'favorite', header: '' },
|
|
122
126
|
{ accessorKey: 'name', header: 'Name' },
|
|
123
127
|
{ accessorKey: 'title', header: 'Title' },
|
|
124
128
|
{ accessorKey: 'source', header: 'Source' },
|
|
@@ -133,6 +137,11 @@ function goToPersona(id: string) {
|
|
|
133
137
|
navigateTo(`/personas/${id}`)
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
// PR86a v3.15.0 — favorites.
|
|
141
|
+
const favs = useFavorites()
|
|
142
|
+
await favs.load()
|
|
143
|
+
const favoritesOnly = ref(false)
|
|
144
|
+
|
|
136
145
|
// PR83b v3.4.0 — bulk selection + delete.
|
|
137
146
|
const toast = useToast()
|
|
138
147
|
const confirmDialog = useConfirmDialog()
|
|
@@ -299,6 +308,15 @@ async function undoTrashIds(ids: string[]) {
|
|
|
299
308
|
aria-label="Filter by source store"
|
|
300
309
|
/>
|
|
301
310
|
|
|
311
|
+
<UButton
|
|
312
|
+
:label="favoritesOnly ? 'All' : 'Favorites'"
|
|
313
|
+
icon="i-lucide-star"
|
|
314
|
+
:color="favoritesOnly ? 'warning' : 'neutral'"
|
|
315
|
+
:variant="favoritesOnly ? 'soft' : 'outline'"
|
|
316
|
+
size="sm"
|
|
317
|
+
@click="favoritesOnly = !favoritesOnly"
|
|
318
|
+
/>
|
|
319
|
+
|
|
302
320
|
<span class="ml-auto text-xs text-muted">
|
|
303
321
|
{{ totalFiltered }} persona{{ totalFiltered !== 1 ? 's' : '' }}
|
|
304
322
|
</span>
|
|
@@ -333,6 +351,16 @@ async function undoTrashIds(ids: string[]) {
|
|
|
333
351
|
@click.stop
|
|
334
352
|
/>
|
|
335
353
|
</template>
|
|
354
|
+
<template #favorite-cell="{ row }">
|
|
355
|
+
<UButton
|
|
356
|
+
icon="i-lucide-star"
|
|
357
|
+
:color="favs.isPersonaFavorite(row.original.id) ? 'warning' : 'neutral'"
|
|
358
|
+
:variant="favs.isPersonaFavorite(row.original.id) ? 'soft' : 'ghost'"
|
|
359
|
+
size="xs"
|
|
360
|
+
:aria-label="favs.isPersonaFavorite(row.original.id) ? 'Unfavorite' : 'Favorite'"
|
|
361
|
+
@click.stop="favs.toggle('personas', row.original.id)"
|
|
362
|
+
/>
|
|
363
|
+
</template>
|
|
336
364
|
<template #name-cell="{ row }">
|
|
337
365
|
<span class="font-medium">{{ row.original.name }}</span>
|
|
338
366
|
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1331,6 +1331,20 @@ def persona_delete(persona_id: str):
|
|
|
1331
1331
|
return {"error": "Persona not found"}
|
|
1332
1332
|
|
|
1333
1333
|
|
|
1334
|
+
# --- Favorites (PR86a v3.15.0) ---
|
|
1335
|
+
|
|
1336
|
+
@app.get("/api/favorites")
|
|
1337
|
+
def favorites_list():
|
|
1338
|
+
from core import favorites as _fav
|
|
1339
|
+
return _fav.list_favorites()
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
@app.post("/api/favorites/{kind}/{item_id}")
|
|
1343
|
+
def favorites_toggle(kind: str, item_id: str):
|
|
1344
|
+
from core import favorites as _fav
|
|
1345
|
+
return _fav.toggle(kind, item_id)
|
|
1346
|
+
|
|
1347
|
+
|
|
1334
1348
|
# --- Global search (PR85d v3.14.0) ---
|
|
1335
1349
|
|
|
1336
1350
|
@app.get("/api/search")
|