arkaos 3.44.0 → 3.46.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/personas/__pycache__/archetypes.cpython-313.pyc +0 -0
- package/dashboard/app/components/NotificationsBell.vue +116 -0
- package/dashboard/app/composables/useActivityFeed.ts +72 -0
- package/dashboard/app/layouts/default.vue +4 -1
- package/dashboard/app/pages/agents/index.vue +7 -0
- package/dashboard/app/pages/personas/index.vue +43 -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 +10 -5
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.46.0
|
|
Binary file
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR93d v3.46.0 — bell-icon notifications popover.
|
|
3
|
+
|
|
4
|
+
const feed = useActivityFeed()
|
|
5
|
+
onMounted(() => feed.load())
|
|
6
|
+
|
|
7
|
+
function formatRelative(iso: string): string {
|
|
8
|
+
const ts = Date.parse(iso)
|
|
9
|
+
if (Number.isNaN(ts)) return iso
|
|
10
|
+
const diff = Date.now() - ts
|
|
11
|
+
const m = Math.floor(diff / 60_000)
|
|
12
|
+
if (m < 1) return 'just now'
|
|
13
|
+
if (m < 60) return `${m}m ago`
|
|
14
|
+
const h = Math.floor(m / 60)
|
|
15
|
+
if (h < 24) return `${h}h ago`
|
|
16
|
+
return `${Math.floor(h / 24)}d ago`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function kindIcon(kind: string): string {
|
|
20
|
+
return {
|
|
21
|
+
success: 'i-lucide-check-circle',
|
|
22
|
+
warning: 'i-lucide-alert-triangle',
|
|
23
|
+
error: 'i-lucide-x-circle',
|
|
24
|
+
info: 'i-lucide-info',
|
|
25
|
+
}[kind] ?? 'i-lucide-circle'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function kindColor(kind: string): string {
|
|
29
|
+
return {
|
|
30
|
+
success: 'text-emerald-500',
|
|
31
|
+
warning: 'text-amber-500',
|
|
32
|
+
error: 'text-rose-500',
|
|
33
|
+
info: 'text-blue-500',
|
|
34
|
+
}[kind] ?? 'text-muted'
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<UPopover :ui="{ content: 'w-80' }">
|
|
40
|
+
<UButton
|
|
41
|
+
icon="i-lucide-bell"
|
|
42
|
+
variant="ghost"
|
|
43
|
+
size="sm"
|
|
44
|
+
aria-label="Notifications"
|
|
45
|
+
:ui="{ base: 'relative' }"
|
|
46
|
+
>
|
|
47
|
+
<UBadge
|
|
48
|
+
v-if="feed.unreadCount.value > 0"
|
|
49
|
+
:label="String(Math.min(feed.unreadCount.value, 99))"
|
|
50
|
+
color="primary"
|
|
51
|
+
size="xs"
|
|
52
|
+
class="absolute -top-1 -right-1 min-w-4"
|
|
53
|
+
/>
|
|
54
|
+
</UButton>
|
|
55
|
+
|
|
56
|
+
<template #content>
|
|
57
|
+
<div class="p-2 border-b border-default flex items-center justify-between">
|
|
58
|
+
<div>
|
|
59
|
+
<p class="text-sm font-semibold">Recent activity</p>
|
|
60
|
+
<p class="text-xs text-muted">
|
|
61
|
+
Last {{ Math.min(feed.events.value.length, 50) }} event{{ feed.events.value.length === 1 ? '' : 's' }}
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
<UButton
|
|
65
|
+
v-if="feed.events.value.length > 0"
|
|
66
|
+
label="Clear"
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="xs"
|
|
69
|
+
@click="feed.clear()"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="max-h-80 overflow-y-auto">
|
|
73
|
+
<p
|
|
74
|
+
v-if="feed.events.value.length === 0"
|
|
75
|
+
class="p-6 text-center text-sm text-muted"
|
|
76
|
+
>
|
|
77
|
+
<UIcon name="i-lucide-bell-off" class="size-6 inline mb-1" /><br>
|
|
78
|
+
Nothing here yet.
|
|
79
|
+
</p>
|
|
80
|
+
<ul v-else class="divide-y divide-default">
|
|
81
|
+
<li
|
|
82
|
+
v-for="ev in feed.events.value"
|
|
83
|
+
:key="ev.id"
|
|
84
|
+
class="px-3 py-2 hover:bg-elevated/40 transition-colors group"
|
|
85
|
+
>
|
|
86
|
+
<component
|
|
87
|
+
:is="ev.to ? 'NuxtLink' : 'div'"
|
|
88
|
+
:to="ev.to"
|
|
89
|
+
class="flex items-start gap-2"
|
|
90
|
+
>
|
|
91
|
+
<UIcon
|
|
92
|
+
:name="kindIcon(ev.kind)"
|
|
93
|
+
:class="['size-4 shrink-0 mt-0.5', kindColor(ev.kind)]"
|
|
94
|
+
/>
|
|
95
|
+
<div class="flex-1 min-w-0">
|
|
96
|
+
<p class="text-sm font-medium truncate">{{ ev.title }}</p>
|
|
97
|
+
<p v-if="ev.description" class="text-xs text-muted truncate">
|
|
98
|
+
{{ ev.description }}
|
|
99
|
+
</p>
|
|
100
|
+
<p class="text-xs text-muted/70 mt-0.5">{{ formatRelative(ev.ts) }}</p>
|
|
101
|
+
</div>
|
|
102
|
+
<UButton
|
|
103
|
+
icon="i-lucide-x"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
size="xs"
|
|
106
|
+
aria-label="Dismiss"
|
|
107
|
+
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
108
|
+
@click.stop.prevent="feed.remove(ev.id)"
|
|
109
|
+
/>
|
|
110
|
+
</component>
|
|
111
|
+
</li>
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
</UPopover>
|
|
116
|
+
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// PR93d v3.46.0 — client-side activity feed.
|
|
2
|
+
//
|
|
3
|
+
// Persistence: localStorage `arkaos_activity_feed` (last 50 events).
|
|
4
|
+
// Events are pushed by `push()` from anywhere in the app. The
|
|
5
|
+
// NotificationsBell component reads + clears.
|
|
6
|
+
|
|
7
|
+
import { createSharedComposable } from '@vueuse/core'
|
|
8
|
+
|
|
9
|
+
export interface ActivityEvent {
|
|
10
|
+
id: string
|
|
11
|
+
ts: string // ISO
|
|
12
|
+
kind: 'success' | 'warning' | 'error' | 'info'
|
|
13
|
+
title: string
|
|
14
|
+
description?: string
|
|
15
|
+
to?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const STORAGE_KEY = 'arkaos_activity_feed'
|
|
19
|
+
const MAX_EVENTS = 50
|
|
20
|
+
|
|
21
|
+
const _useActivityFeed = () => {
|
|
22
|
+
const events = useState<ActivityEvent[]>('activityFeed', () => [])
|
|
23
|
+
const loaded = useState<boolean>('activityFeedLoaded', () => false)
|
|
24
|
+
const unreadCount = computed(() => events.value.length)
|
|
25
|
+
|
|
26
|
+
function _persist() {
|
|
27
|
+
if (typeof window === 'undefined') return
|
|
28
|
+
try {
|
|
29
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(events.value))
|
|
30
|
+
} catch {
|
|
31
|
+
// Storage full or disabled — silently drop persistence.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function load() {
|
|
36
|
+
if (loaded.value || typeof window === 'undefined') return
|
|
37
|
+
try {
|
|
38
|
+
const raw = window.localStorage.getItem(STORAGE_KEY)
|
|
39
|
+
if (raw) {
|
|
40
|
+
const parsed = JSON.parse(raw)
|
|
41
|
+
if (Array.isArray(parsed)) {
|
|
42
|
+
events.value = parsed.slice(0, MAX_EVENTS)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
events.value = []
|
|
47
|
+
}
|
|
48
|
+
loaded.value = true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function push(entry: Omit<ActivityEvent, 'id' | 'ts'>) {
|
|
52
|
+
load()
|
|
53
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
54
|
+
const ts = new Date().toISOString()
|
|
55
|
+
events.value = [{ id, ts, ...entry }, ...events.value].slice(0, MAX_EVENTS)
|
|
56
|
+
_persist()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clear() {
|
|
60
|
+
events.value = []
|
|
61
|
+
_persist()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function remove(id: string) {
|
|
65
|
+
events.value = events.value.filter((e) => e.id !== id)
|
|
66
|
+
_persist()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { events, unreadCount, load, push, clear, remove }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const useActivityFeed = createSharedComposable(_useActivityFeed)
|
|
@@ -133,7 +133,10 @@ const links = [[{
|
|
|
133
133
|
<span class="text-xl font-bold text-primary">A</span>
|
|
134
134
|
<span v-if="!collapsed" class="font-semibold">ArkaOS</span>
|
|
135
135
|
</div>
|
|
136
|
-
<
|
|
136
|
+
<div v-if="!collapsed" class="flex items-center gap-1">
|
|
137
|
+
<NotificationsBell />
|
|
138
|
+
<UColorModeButton size="xs" />
|
|
139
|
+
</div>
|
|
137
140
|
</div>
|
|
138
141
|
</template>
|
|
139
142
|
|
|
@@ -360,6 +360,13 @@ async function bulkDelete() {
|
|
|
360
360
|
? [{ label: 'Undo', icon: 'i-lucide-rotate-ccw', onClick: () => undoTrashIds(trashIds) }]
|
|
361
361
|
: undefined,
|
|
362
362
|
})
|
|
363
|
+
// PR93d v3.46.0 — also surface in the notifications bell.
|
|
364
|
+
useActivityFeed().push({
|
|
365
|
+
kind: failures > 0 ? 'warning' : 'success',
|
|
366
|
+
title: `Deleted ${successes.length} agent${successes.length === 1 ? '' : 's'}`,
|
|
367
|
+
description: failures > 0 ? `${failures} skipped` : 'Undo via /trash',
|
|
368
|
+
to: '/trash',
|
|
369
|
+
})
|
|
363
370
|
} else {
|
|
364
371
|
toast.add({
|
|
365
372
|
title: 'Nothing deleted',
|
|
@@ -142,6 +142,41 @@ const favs = useFavorites()
|
|
|
142
142
|
await favs.load()
|
|
143
143
|
const favoritesOnly = ref(false)
|
|
144
144
|
|
|
145
|
+
// PR93c v3.45.0 — bulk export only selected rows.
|
|
146
|
+
const exportingSelected = ref(false)
|
|
147
|
+
async function exportSelectedZip() {
|
|
148
|
+
if (selected.value.size === 0) return
|
|
149
|
+
const ids = Array.from(selected.value).join(',')
|
|
150
|
+
exportingSelected.value = true
|
|
151
|
+
try {
|
|
152
|
+
const blob = await $fetch<Blob>(
|
|
153
|
+
`${apiBase}/api/personas/export-all.zip`,
|
|
154
|
+
{ query: { ids }, responseType: 'blob' },
|
|
155
|
+
)
|
|
156
|
+
const url = URL.createObjectURL(blob)
|
|
157
|
+
const a = document.createElement('a')
|
|
158
|
+
a.href = url
|
|
159
|
+
a.download = `arkaos-personas-selected-${selected.value.size}.zip`
|
|
160
|
+
document.body.appendChild(a)
|
|
161
|
+
a.click()
|
|
162
|
+
a.remove()
|
|
163
|
+
URL.revokeObjectURL(url)
|
|
164
|
+
toast.add({
|
|
165
|
+
title: `Exported ${selected.value.size} persona${selected.value.size === 1 ? '' : 's'}`,
|
|
166
|
+
color: 'success',
|
|
167
|
+
icon: 'i-lucide-archive',
|
|
168
|
+
})
|
|
169
|
+
} catch (err) {
|
|
170
|
+
toast.add({
|
|
171
|
+
title: 'Export failed',
|
|
172
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
173
|
+
color: 'error',
|
|
174
|
+
})
|
|
175
|
+
} finally {
|
|
176
|
+
exportingSelected.value = false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
145
180
|
// PR92a v3.39.0 — bulk export every persona as a zip.
|
|
146
181
|
const exportingZip = ref(false)
|
|
147
182
|
async function exportAllAsZip() {
|
|
@@ -636,6 +671,14 @@ async function undoTrashIds(ids: string[]) {
|
|
|
636
671
|
@click="clearSelection"
|
|
637
672
|
/>
|
|
638
673
|
<div class="h-5 w-px bg-default" />
|
|
674
|
+
<UButton
|
|
675
|
+
label="Export ZIP"
|
|
676
|
+
icon="i-lucide-archive"
|
|
677
|
+
size="sm"
|
|
678
|
+
variant="soft"
|
|
679
|
+
:loading="exportingSelected"
|
|
680
|
+
@click="exportSelectedZip"
|
|
681
|
+
/>
|
|
639
682
|
<UButton
|
|
640
683
|
label="Delete"
|
|
641
684
|
icon="i-lucide-trash-2"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -334,13 +334,11 @@ def personas_archetypes():
|
|
|
334
334
|
|
|
335
335
|
|
|
336
336
|
@app.get("/api/personas/export-all.zip")
|
|
337
|
-
def personas_export_all():
|
|
337
|
+
def personas_export_all(ids: Optional[str] = None):
|
|
338
338
|
"""PR92a v3.39.0 — stream every persona as Markdown inside a ZIP.
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`ObsidianPersonaStore._render`, and zips them. Filename uses the
|
|
343
|
-
persona name (sanitised), falling back to id.
|
|
340
|
+
PR93c v3.45.0 — optional ``?ids=a,b,c`` filter narrows the export
|
|
341
|
+
to a specific subset.
|
|
344
342
|
"""
|
|
345
343
|
mgr = _get_persona_manager()
|
|
346
344
|
if not mgr:
|
|
@@ -350,6 +348,13 @@ def personas_export_all():
|
|
|
350
348
|
except Exception as exc: # noqa: BLE001
|
|
351
349
|
return {"error": f"list failed: {exc}"}
|
|
352
350
|
|
|
351
|
+
# PR93c — optional id allow-list.
|
|
352
|
+
id_filter: Optional[set[str]] = None
|
|
353
|
+
if ids:
|
|
354
|
+
id_filter = {s.strip() for s in ids.split(",") if s.strip()}
|
|
355
|
+
if id_filter is not None:
|
|
356
|
+
items = [p for p in items if hasattr(p, "id") and p.id in id_filter]
|
|
357
|
+
|
|
353
358
|
from core.personas.obsidian_store import ObsidianPersonaStore
|
|
354
359
|
|
|
355
360
|
import io
|